package redpacket.service.impl;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import redpacket.dto.CreatePacketRequest;
import redpacket.dto.GrabResponse;
import redpacket.mapper.RedPacketMapper;
import redpacket.model.RedPacket;
import redpacket.mq.GrabProducer;
import redpacket.service.RedPacketService;
import redpacket.util.Snowflake;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedPacketServiceImpl implements RedPacketService {

    private StringRedisTemplate redisTemplate;
    private DefaultRedisScript<Long> robRedPacketScript;
    private final StringRedisTemplate redis;
    private final RedPacketMapper redPacketMapper;
    private final GrabProducer grabProducer;
    private final Snowflake snowflake = new Snowflake(1, 1);

    private String listKey(long packetId) {
        return "rp:%d:list".formatted(packetId);
    }

    private String userKey(long packetId, long userId) {
        return "rp:%d:user:%d".formatted(packetId, userId);
    }

    @Override
    @Transactional
    public Long create(CreatePacketRequest req) {
        long id = snowflake.nextId();
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime expireAt = now.plusSeconds(req.getExpireSeconds());

        // Pre-split amounts
        List<BigDecimal> amounts = (req.getType() == 1) ? splitAvg(req.getTotalAmount(), req.getTotalCount()) : splitRandom(req.getTotalAmount(), req.getTotalCount());

        // Push to Redis list
        List<String> asStr = amounts.stream().map(a -> a.setScale(2, RoundingMode.DOWN).toPlainString()).toList();
        redis.opsForList().leftPushAll(listKey(id), asStr);
        redis.expireAt(listKey(id), java.sql.Timestamp.valueOf(expireAt).toInstant());

        // Insert DB main record
        RedPacket rp = new RedPacket();
        rp.setId(id);
        rp.setSenderId(req.getSenderId());
        rp.setTotalAmount(req.getTotalAmount());
        rp.setTotalCount(req.getTotalCount());
        rp.setRemainCount(req.getTotalCount());
        rp.setType(req.getType());
        rp.setStatus(1);
        rp.setExpireTime(expireAt);
        rp.setCreateTime(now);
        redPacketMapper.insert(rp);

        return id;
    }


    public String rob(Long redPacketId, Long userId) {
        String stockKey = "red_packet:" + redPacketId + ":stock";
        String usersKey = "red_packet:" + redPacketId + ":users";

        Long result = redisTemplate.execute(
                robRedPacketScript,
                Arrays.asList(stockKey, usersKey),
                userId.toString()
        );

        if (result == 0) {
            return "红包已抢完";
        } else if (result == 2) {
            return "你已经抢过了";
        } else {
            return "抢红包成功";
        }
    }

    @Override
    public GrabResponse grab(Long packetId, Long userId) {
        // idempotency: SETNX
        String uKey = userKey(packetId, userId);
        Boolean ok = redis.opsForValue().setIfAbsent(uKey, "1", 10, TimeUnit.MINUTES);
        if (Boolean.FALSE.equals(ok)) {
            return new GrabResponse(false, "已经抢过了", null);
        }

        String amt = redis.opsForList().leftPop(listKey(packetId));
        if (amt == null) {
            return new GrabResponse(false, "已抢完", null);
        }

        // send MQ to persist
        grabProducer.send(packetId, userId, new BigDecimal(amt));

        return new GrabResponse(true, "抢到啦", new BigDecimal(amt));
    }

    private List<BigDecimal> splitAvg(BigDecimal total, int count) {
        List<BigDecimal> res = new ArrayList<>();
        BigDecimal avg = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.DOWN);
        BigDecimal sum = BigDecimal.ZERO;
        for (int i = 0; i < count - 1; i++) {
            res.add(avg);
            sum = sum.add(avg);
        }
        res.add(total.subtract(sum).setScale(2, RoundingMode.DOWN));
        return res;
    }

    private List<BigDecimal> splitRandom(BigDecimal total, int count) {
        // Simple random split ensuring min 0.01
        List<BigDecimal> res = new ArrayList<>();
        BigDecimal remain = total;
        int remainCount = count;
        for (int i = 0; i < count - 1; i++) {
            // max random up to 2 * (remain / remainCount)
            BigDecimal max = remain.divide(BigDecimal.valueOf(remainCount), 2, RoundingMode.DOWN).multiply(BigDecimal.valueOf(2));
            BigDecimal r = BigDecimal.valueOf(Math.random()).multiply(max);
            if (r.compareTo(new BigDecimal("0.01")) < 0) r = new BigDecimal("0.01");
            r = r.setScale(2, RoundingMode.DOWN);
            if (remain.subtract(r).compareTo(new BigDecimal("0.01").multiply(BigDecimal.valueOf(remainCount - 1))) < 0) {
                r = new BigDecimal("0.01");
            }
            res.add(r);
            remain = remain.subtract(r);
            remainCount--;
        }
        res.add(remain.setScale(2, RoundingMode.DOWN));
        return res;
    }
}
